﻿//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//    SystemUsageMetrics.cs
//
// Abstract:
//    
//    Updates metrics of Enhanced Reminders system usage - e.g. #reminders delivered over SMS etc.
//
//    Note: THIS CODE IS NOT TO BE SHIPPED.
//
//--

using System;
using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Microsoft.OfficeCommunicationsServer.Applications.Common;
using Microsoft.EnhancedReminders;

namespace Microsoft.EnhancedRemindersTools
{
    /// <summary>
    /// Helper class to represent a time interval for computing the peak time.
    /// </summary>
    internal sealed class TimeInterval
    {
        internal const int IntervalSizeInMinutes = 30; // The size of the interval in minutes.
        internal const int MaxPeakIntervals = 5; // The number of peak intervals to find.

        private readonly DateTime _startTime;
        private int _count;

        internal TimeInterval(DateTime startTime)
        {
            _startTime = startTime;
        }

        internal void IncrementCount()
        {
            ++_count;
        }

        internal DateTime StartTime
        {
            get
            {
                return _startTime;
            }
        }

        internal DateTime EndTime
        {
            get
            {
                return _startTime.AddMinutes(IntervalSizeInMinutes);
            }
        }

        internal int Count
        {
            get
            {
                return _count;
            }
        }
    }

    /// <summary>
    /// Updates the system usage metrics from the relevant trace messages.
    /// </summary>
    internal sealed class SystemUsageMetrics
    {
        private static Regex _resultRegex =
            new Regex(@"Notification result: status=(?<status>[^,]+), channel=(?<channel>[^,]+), type=(?<type>[^,]+), manual=(?<manual>(True|False)), joinNowType=(?<joinnowtype>.+)",
                RegexOptions.Compiled);

        // $$TODO:  The NotificationResult needs to log more detailed channel type in  (i.e. OCS 
        // vs custom and work phone etc).  More granular counters will be added when that happens.

        // Statistics for successfully delivered notifications.
        private int _totalNotifications;
        private int _voicemailNotifications;
        private int _meetingNotifications;
        private int _notificationsToCommunicator;
        private int _notificationsToPhone;
        private int _notificationsToSMS;
        private int _manualNotifications;
        private int _failedNotifications; // Counts all other failed notifications.

        // Auto join statistics (for successfull auto-joins)
        private int _totalAutoJoins;
        private int _liveMeetings;
        private int _conferenceCalls;
        private int _autoJoinsViaCommunicator;
        private int _autoJoinsViaPhone;
        private int _manualAutoJoins;
        private int _totalFailedAutoJoins; // Tracks all failed auto join deliveries (regardless of reason)
        private int _failedToJoinConference; // Tracks failures to enter the live meeting / conference call

        // Track the top-N peak usage times.
        private TimeInterval _currentInterval;
        private List<TimeInterval> _peakIntervals = new List<TimeInterval>();

        /// <summary>
        /// Update user metrics if the message string is relevant.
        /// </summary>
        /// <param name="message">The log message string to analyze.</param>
        internal void Update(DateTime timeStamp, string message)
        {
            if (String.IsNullOrEmpty(message) ||
                !message.StartsWith("Notification result: status="))
            {
                return;
            }

            Match match = _resultRegex.Match(message);
            if (!match.Success)
            {
                Console.WriteLine("Error parsing the notification result string: {0}", message);
                return;
            }

            try
            {
                // Extract the various fields from the notification result string.
                NotificationResultStatus status = Helpers.AssertCast<NotificationResultStatus>(Enum.Parse(typeof(NotificationResultStatus), match.Groups["status"].Value));
                NotificationChannelType channelType = Helpers.AssertCast<NotificationChannelType>(Enum.Parse(typeof(NotificationChannelType), match.Groups["channel"].Value));
                Type notificationItemType = GetReminderType(match.Groups["type"].Value);
                bool isManualReminder = bool.Parse(match.Groups["manual"].Value);
                ConferenceType conferenceType = Helpers.AssertCast<ConferenceType>(Enum.Parse(typeof(ConferenceType), match.Groups["joinnowtype"].Value));

                if (conferenceType == ConferenceType.None) // Update ER notification metrics.
                {
                    UpdateNotificationMetrics(status, channelType, notificationItemType, isManualReminder);
                }
                else // Update auto join metrics.
                {
                    UpdateAutoJoinMetrics(status, channelType, isManualReminder, conferenceType);
                }
            }
            catch (FormatException exn)
            {
                Console.WriteLine("The following error occurred while parsing the notification result string '{0}: {1}", message, exn.ToString());
                return;
            }
            catch (ArgumentException exn)
            {
                Console.WriteLine("The following error occurred while parsing the notification result string '{0}: {1}", message, exn.ToString());
                return;
            }

            UpdatePeakIntervals(timeStamp);
        }

        /// <summary>
        /// Helper method to convert the type string for a NotificationItem type to a Type.
        /// </summary>
        /// <param name="notificationItemType">The NotificationItem type string description.</param>
        /// <returns>The appropriate NotificationItem type or null if not found.</returns>
        private Type GetReminderType(string notificationItemType)
        {
            if (String.IsNullOrEmpty(notificationItemType))
            {
                return null;
            }

            Type[] types = new Type[] { typeof(VoicemailNotificationItem), typeof(CalendarNotificationItem), typeof(MeetingNotificationItem) };

            foreach (Type type in types)
            {
                if (notificationItemType == type.ToString())
                {
                    return type;
                }
            }

            Debug.Fail("Unable to match type string to a reminder type.  Has a new type been added?");

            return null;
        }

        /// <summary>
        /// Update notification metrics.
        /// </summary>
        /// <param name="status">The notification result status.</param>
        /// <param name="channelType">The channel type.</param>
        /// <param name="notificationItemType">The NotificationItem type.</param>
        /// <param name="isManualReminder">True if the reminder was manually created; false otherwise.</param>
        private void UpdateNotificationMetrics(NotificationResultStatus status, NotificationChannelType channelType, Type notificationItemType, bool isManualReminder)
        {
            Debug.Assert(notificationItemType != null, "notificationItemType");

            if (status != NotificationResultStatus.Success)
            {
                Debug.Assert(status != NotificationResultStatus.JoinNowSucceeded);
                ++_failedNotifications;
                return;
            }

            ++_totalNotifications;

            if (notificationItemType == typeof(VoicemailNotificationItem))
            {
                Debug.Assert(!isManualReminder, "Voicemail notifications are rule generated");
                ++_voicemailNotifications;
            }
            else if (notificationItemType == typeof(CalendarNotificationItem) ||
                     notificationItemType == typeof(MeetingNotificationItem))
            {
                ++_meetingNotifications;
                if (isManualReminder)
                {
                    ++_manualNotifications;
                }
            }
            else
            {
                Debug.Fail("Unexpected NotificationItem type for notifications");
            }

            switch (channelType)
            {
                case NotificationChannelType.Communicator:
                    ++_notificationsToCommunicator;
                    break;
                case NotificationChannelType.Phone:
                    ++_notificationsToPhone;
                    break;
                case NotificationChannelType.SMS:
                    ++_notificationsToSMS;
                    break;
                default:
                    Debug.Fail("Unexpected channel type for delivered notification");
                    break;
            }
        }

        /// <summary>
        /// Update auto join metrics.
        /// </summary>
        /// <param name="status">The notification result status.</param>
        /// <param name="channelType">The channel type.</param>
        /// <param name="isManualReminder">True for a manually created reminder; false otherwise.</param>
        /// <param name="conferenceType">The conference type.</param>
        private void UpdateAutoJoinMetrics(NotificationResultStatus status, NotificationChannelType channelType, bool isManualReminder, ConferenceType conferenceType)
        {
            if (status != NotificationResultStatus.JoinNowSucceeded)
            {
                ++_totalFailedAutoJoins;
                if (status == NotificationResultStatus.Success)
                {
                    ++_failedToJoinConference; // Notification delivered but failed to join the conference.
                }
                return;
            }

            ++_totalAutoJoins;

            switch (conferenceType)
            {
                case ConferenceType.LiveMeeting:
                    ++_liveMeetings;
                    break;

                case ConferenceType.ConferenceCall:
                    ++_conferenceCalls;
                    break;

                default:
                    Debug.Fail("Unexpected conference type");
                    break;
            }

            switch (channelType)
            {
                case NotificationChannelType.Communicator:
                    ++_autoJoinsViaCommunicator;
                    break;
                case NotificationChannelType.Phone:
                    ++_autoJoinsViaPhone;
                    break;
                default:
                    Debug.Fail("Unexpected channel type for delivered notification");
                    break;
            }

            if (isManualReminder)
            {
                ++_manualAutoJoins;
            }
        }

        /// <summary>
        /// Update the time interval for the notification message and 
        /// </summary>
        /// <param name="timeStamp"></param>
        private void UpdatePeakIntervals(DateTime timeStamp)
        {
            // Create the very first interval.
            if (_currentInterval == null)
            {
                _currentInterval = new TimeInterval(timeStamp);
            }

            // Currently inside an interval - update the counter.
            if (timeStamp.CompareTo(_currentInterval.EndTime) < 0)
            {
                _currentInterval.IncrementCount();
                return;
            }

            Debug.Assert(_peakIntervals != null, "_peakIntervals");

            // Gone past the interval boundary - check if this is one of the top-N peak intervals.
            if (_peakIntervals.Count == 0)
            {
                _peakIntervals.Add(_currentInterval);
            }
            else // Check if interval should be in the peak list and insert in appropriate sort-order location.
            {
                for (int i = 0; i < _peakIntervals.Count; i++) 
                {
                    if (_currentInterval.Count > _peakIntervals[i].Count)
                    {
                        _peakIntervals.Insert(i, _currentInterval);
                        if (_peakIntervals.Count > TimeInterval.MaxPeakIntervals) // Added one more to the top-N list, so remove.
                        {
                            _peakIntervals.RemoveAt(TimeInterval.MaxPeakIntervals);
                        }
                        break;
                    }
                }
            }

            Debug.Assert(TimeInterval.IntervalSizeInMinutes > 0, "Interval size should be > 0");

            // Compute the new interval start point.
            long intervalsSinceLastOne = 
                (timeStamp.Ticks - _currentInterval.EndTime.Ticks) / (TimeSpan.TicksPerMinute * TimeInterval.IntervalSizeInMinutes);

            // Create the new time interval instance for the new start-end boundaries.
            _currentInterval = new TimeInterval(_currentInterval.EndTime.AddMinutes(TimeInterval.IntervalSizeInMinutes * intervalsSinceLastOne));
        }

        /// <summary>
        /// Write the user data metrics out to the summary file.
        /// </summary>
        /// <param name="summaryFile">The summary file.</param>
        internal void WriteData(StreamWriter summaryFile)
        {
            Debug.Assert(summaryFile != null, "summaryFile");

            // Output metrics for Enhanced Reminders notifications.
            summaryFile.WriteLine("Total number of successful ER notifications delivered: {0}", _totalNotifications);
            summaryFile.WriteLine("Total number of voicemail notifications delivered: {0}", _voicemailNotifications);
            summaryFile.WriteLine("Total number of meeting notifications delivered: {0}", _meetingNotifications);
            summaryFile.WriteLine("Number of notifications delivered per channel type: Communicator - {0}, Phone - {1}, SMS - {2}", _notificationsToCommunicator, _notificationsToPhone, _notificationsToSMS);
            summaryFile.WriteLine("Number of manually created notifications delivered: {0}", _manualNotifications);
            summaryFile.WriteLine("Number of failed ER notification attempts: {0}", _failedNotifications);

            // Output metrics for auto join.
            summaryFile.WriteLine("Total number of successful auto joins: {0}", _totalAutoJoins);
            summaryFile.WriteLine("Number of Live Meetings joined: {0}", _liveMeetings);
            summaryFile.WriteLine("Number of conference calls joined: {0}", _conferenceCalls);
            summaryFile.WriteLine("Number of auto joins per channel type: Communicator - {0}, Phone - {1}", _autoJoinsViaCommunicator, _autoJoinsViaPhone);
            summaryFile.WriteLine("Number of manually created auto joins: {0}", _manualAutoJoins);
            summaryFile.WriteLine("Total number of failed auto join deliveries: {0}", _totalFailedAutoJoins);
            summaryFile.WriteLine("Number of failed attempts to join the meeting/conference: {0}", _failedToJoinConference);

            // Output the peak intervals.
            summaryFile.Write("Peak times for ER delivery (in descending order): ");
            foreach (TimeInterval interval in _peakIntervals)
            {
                summaryFile.Write("Start - {0} End - {1} Notifications - {2}", interval.StartTime.ToString(), interval.EndTime.ToString(), interval.Count);
            }       
            summaryFile.WriteLine();
        }
    }
}
